﻿using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Threading.Channels;
using StackExchange.Redis;

namespace WaferService
{
    public class SubstrateTrackingService
    {
        static readonly JsonSerializerOptions EventSerializerOptions = new()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
        };

        const string LastEventId = "LastEventId";

        readonly Channel<SubstrateTrackingEvent> channel = Channel.CreateBounded<SubstrateTrackingEvent>(10);

        readonly IOptions<EFEMConfig> options;
        readonly IEFEMService efemService;
        readonly ConnectionMultiplexer redis;
        readonly IDbContextFactory<ApplicationDbContext> factory;

        public SubstrateTrackingService(ILogger<SubstrateTrackingService> logger,
            IOptions<EFEMConfig> options,
            IEFEMService efemService,
            IPLCService plcService,
            ConnectionMultiplexer redis,
            IDbContextFactory<ApplicationDbContext> factory)
        {
            this.options = options;
            this.efemService = efemService;
            this.redis = redis;
            this.factory = factory;

            Task.Run(async () =>
            {
                await foreach (var evt in channel.Reader.ReadAllAsync())
                {
                    try
                    {
                        await HandleEvent(evt);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "event error");
                    }
                }
            });
        }

        public async Task SendEvent(SubstrateTrackingEvent evt)
        {
            await channel.Writer.WriteAsync(evt);
        }

        public async Task<SubstrateTrackingResult> Track()
        {
            using var ctx = factory.CreateDbContext();
            var query = ctx.Substrates.ToListAsync();

            var config = options.Value;
            var foups = new List<Foup>();
            for (var i = 0; i < config.LoadPort; i++)
            {
                var status = await efemService.GetLoadPortStatus(i + 1);
                foups.Add(new Foup
                {
                    Slots = new WaferState[25],
                    IsOpen = status.Clamp,
                    Placement = status.Placement,
                });
            }

            var strayWafers = new List<StrayWafer>();
            var substrates = await query;
            foreach (var s in substrates)
            {
                if (s.Foup > config.LoadPort)
                {
                    throw new BadHttpRequestException("Substrates tracking is out of sync");
                }

                if (s.Location != WaferLocation.Foup)
                {
                    strayWafers.Add(new StrayWafer
                    {
                        Location = s.Location,
                        Foup = s.Foup,
                        Slot = s.Slot
                    });
                }

                foups[s.Foup - 1].Slots[s.Slot - 1] = s.State;
            }

            var id = await ctx.KeyValues
                .Where(kv => kv.Key == LastEventId)
                .Select(kv => kv.Value)
                .FirstOrDefaultAsync() ?? "0";
            return new SubstrateTrackingResult
            {
                LastEventId = id,
                Foups = foups,
                StrayWafers = strayWafers
            };
        }

        async Task HandleEvent(SubstrateTrackingEvent evt)
        {
            var db = redis.GetDatabase();
            var json = JsonSerializer.Serialize(evt, EventSerializerOptions);
            var id = (string?)await db.StreamAddAsync(Key.SubstrateTrackingStream, "event", json);

            using var ctx = factory.CreateDbContext();
            using var transaction = ctx.Database.BeginTransaction();
            await ctx.Database.ExecuteSqlAsync($"""
            INSERT INTO KeyValue(Key, Value) VALUES({LastEventId},{id!}) 
                ON CONFLICT(Key) DO UPDATE SET Value=excluded.Value
            """);

            switch (evt)
            {
                case FoupOpenEvent open:
                    await ctx.Substrates
                        .Where(s => s.Foup == open.Foup && s.Location == WaferLocation.Foup)
                        .ExecuteDeleteAsync();
                    var states = open.Slots;
                    for (var i = 0; i < states.Length; i++)
                    {
                        switch (open.Slots[i])
                        {
                            case WaferState.Present:
                            case WaferState.MappingError:
                                ctx.Substrates.Add(new Substrate
                                {
                                    Foup = open.Foup,
                                    Slot = i + 1,
                                    Location = WaferLocation.Foup,
                                    State = states[i]
                                });
                                break;
                        }
                    }
                    await ctx.SaveChangesAsync();
                    break;
                case FoupClosedEvent closed:
                    await ctx.Substrates
                        .Where(s => s.Foup == closed.Foup && s.Location == WaferLocation.Foup)
                        .ExecuteDeleteAsync();
                    break;
                case WaferTransferEvent moved:
                    await ctx.Substrates
                        .Where(s => s.Foup == moved.Foup && s.Slot == moved.Slot)
                        .ExecuteUpdateAsync(setters => setters
                            .SetProperty(s => s.Location, moved.To));
                    break;
                case WaferStateChangedEvent changed:
                    await ctx.Substrates
                        .Where(s => s.Foup == changed.Foup && s.Slot == changed.Slot)
                        .ExecuteUpdateAsync(setters => setters
                            .SetProperty(s => s.State, changed.State));
                    break;
                case WaferSwapEvent swap:
                    await ctx.Substrates
                        .Where(s => s.Foup == swap.FoupIn && s.Slot == swap.SlotIn)
                        .ExecuteUpdateAsync(setters => setters
                            .SetProperty(s => s.Location, swap.LocationIn));
                    await ctx.Substrates
                        .Where(s => s.Foup == swap.FoupOut && s.Slot == swap.SlotOut)
                        .ExecuteUpdateAsync(setters => setters
                            .SetProperty(s => s.Location, swap.LocationOut));
                    break;
                case WaferTransferEndEvent:
                    db.StreamTrim(Key.SubstrateTrackingStream, 1000, true, CommandFlags.FireAndForget);
                    break;
            }

            await transaction.CommitAsync();
        }
    }
}
